﻿using System;
using System.Collections;
using System.IO;
using System.Threading;
using Microsoft.SPOT;
using Microsoft.SPOT.Presentation;
using Microsoft.SPOT.Presentation.Controls;
using Microsoft.SPOT.Presentation.Media;
using Microsoft.SPOT.Touch;

using Dairy_Town.Elm327.Core;

using Gadgeteer.Networking;
using GT = Gadgeteer;
using GTM = Gadgeteer.Modules;
using Gadgeteer;
using Gadgeteer.Interfaces;
using System.Text;
using System.Net;
using System.Text.RegularExpressions;
using Dairy_Town.Models;

namespace Dairy_Town
{
    public class DeviceInformation
    {
        private DeviceInformation _instance;
        private DeviceInformation instance
        {
            get
            {
                if (_instance == null)
                    _instance = new DeviceInformation();

                return _instance;
            }
        }

        private DeviceInformation()
        {
            
        }

        public static string UniqueDeviceIdentifier()
        {
            var udid = ExtendedWeakReference.RecoverOrCreate(typeof(string), 0, ExtendedWeakReference.c_SurviveBoot | ExtendedWeakReference.c_SurvivePowerdown);

            return udid.Target.ToString();
        }
    }

    public partial class Program
    {

#error Values for "GatewayAddress", "CloudServiceAddress", and "CloudServicePort" must be updated in config.xml, this error may be deleted once configured.
        private static string myGatewayAddress = ConfigurationManager.GetAppSetting("GatewayAddress");
        private static string TOWER_HOST = ConfigurationManager.GetAppSetting("CloudServiceAddress");
        private static UInt32 TOWER_PORT = UInt32.Parse( ConfigurationManager.GetAppSetting("CloudServicePort") );

        Accelerometer       accelerometer;
        LocationManager     geoLocation;
        Logger              logger;
        OBD                 obd;
        WifiAdapter         wifi;
                
        void ProgramStarted()
            {
            /*******************************************************************************************
             
            Modules added in the Program.gadgeteer designer view are used by typing 
            their name followed by a period, e.g.  button.  or  camera.

            *******************************************************************************************/

            // Use Debug.Print to show messages in Visual Studio's "Output" window during debugging.
            Version version = new Version(1, 1, 3, 118);

            Console.AttachDisplay(this.oledDisplay);
            Console.Display("Program Started v" + version.Major + "." + version.Minor + "." + version.Build);

            var sysOEM = Microsoft.SPOT.Hardware.SystemInfo.SystemID.OEM;
            var sysModel = Microsoft.SPOT.Hardware.SystemInfo.SystemID.Model;
            var sysSKU = Microsoft.SPOT.Hardware.SystemInfo.SystemID.SKU;
            var sysVer = Microsoft.SPOT.Hardware.SystemInfo.Version;

            Debug.Print(sysOEM + "; " + sysModel + "; " + sysSKU + "; " + sysVer.Major.ToString() + "." + sysVer.Minor);

            extender.DebugPrintEnabled      = false;
            //gps.DebugPrintEnabled           = true;
            sdCard.DebugPrintEnabled        = false;
            rs232.DebugPrintEnabled         = false;
            wifi_RS21.DebugPrintEnabled     = true;
            
            // Creates a new instance of the Logger class, which performs write operations on the SD card.
            logger = new Logger(this.sdCard);
            logger.RecordingTick += (log =>
                {
                    Snapshot snapshot = new Snapshot();
                    snapshot.VIN = (obd == null) ? "..." : obd.VIN;
                    snapshot.Speed = (obd == null) ? 0.0 : obd.Speed;
                    snapshot.XAcceleration = (accelerometer == null) ? 0.0 : accelerometer.X;
                    snapshot.YAcceleration = (accelerometer == null) ? 0.0 : accelerometer.Y;
                    snapshot.ZAcceleration = (accelerometer == null) ? 0.0 : accelerometer.Z;
                    snapshot.Latitude = (geoLocation == null) ? 0.0 : geoLocation.Latitude;
                    snapshot.Longitude = (geoLocation == null) ? 0.0 : geoLocation.Longitude;
                    snapshot.Heading = (geoLocation == null) ? 0.0 : geoLocation.Heading;
                    snapshot.GPSFix = (geoLocation == null) ? DateTime.Now : geoLocation.Timestamp;
                    snapshot.Version = version.Major + "." + version.Minor;

                    log.Write(snapshot);
                });

            accelerometer = new Accelerometer(this.extender);
            accelerometer.debugEnabled(false);

            geoLocation = new LocationManager(this.gps);

            obd = new OBD(this.rs232);
            obd.ObdConnected += obd_ObdConnected;
            obd.ObdDisconnected += obd_ObdDisconnected;

            wifi = new WifiAdapter(this.wifi_RS21);
            wifi.GatewayAvailable += wifi_GatewayAvailable; 
        }

        // List of samples to be validated as the program is running.
        private ArrayList samples = new ArrayList();

        /// <summary>
        /// This function checks to determine whether or not the GPS is receiving new coordinates at least every three seconds.
        /// </summary>
        private void ValidateSamples()
        {
            if (samples.Count >= 3)
            {
                Snapshot expired = (Snapshot)samples[0];                    // Least recent stored Snapshot.
                Snapshot recent = (Snapshot)samples[samples.Count - 1];     // Most recent stored Snapshot.
                Snapshot middle = (Snapshot)samples[1];                     // Snapshot between the least and most recent.

                #region Validate GPS

                bool noVelocity = false;                                                    // Boolean to determine whether or not the vehicle is currently moving.

                // If a sample in the list has a speed less than 10 mph, the car is considered for the purposes of this validation to be stationary.
                foreach (Snapshot sample in samples)
                {
                    if (sample.Speed <= 10)
                        noVelocity = true;
                }

                if (!noVelocity)
                {
                    if (recent.Latitude.Equals(expired.Latitude) && recent.Longitude.Equals(expired.Longitude)) // GPS readings are the same despite the vehicle traveling
                        geoLocation.Reset();
                }

                #endregion

                #region Validate Speedometer

                bool noDistance = false;                                                    // Boolean to determine whether or not the car has traveled any distance.

                if (!recent.Latitude.Equals(expired.Latitude) || !recent.Longitude.Equals(expired.Longitude))
                {
                    // GPS readings are different.
                    noDistance = true;
                }

                if (!noDistance)
                {
                    // If the speed from each of the last three Snapshots is 0 mph, but the GPS signals indicate a location change, reset the OBD device.
                    if (recent.Speed == 0.0 && expired.Speed == 0.0 && middle.Speed == 0.0)
                        obd.Reset();
                }

                #endregion

                // Remove a sample from the list, maintaining a count of three whenever this method is called.
                samples.RemoveAt(0);
            }
        }

        #region Event Handlers

        /// <summary>
        /// Stops the vehicle from collecting Snapshots if OBD connection is lost, not currently in use.
        /// </summary>
        private void obd_ObdDisconnected(object sender, EventArgs e)
        {
            if (logger != null)
                logger.StopCollection();
        }

        /// <summary>
        /// Waits three seconds to verify that the OBD is connected and running before signaling to the vehicle to start monitoring.
        /// </summary>
        private void obd_ObdConnected(object sender, EventArgs e)
        {
            if (logger != null)
            {
                int verify = 0;
                GT.Timer obdTimeout = new GT.Timer(3000);
                obdTimeout.Tick += (GT.Timer timer) =>
                    {
                        Debug.Print("Check obd connection");
                        if (verify >= 2)
                        {
                            timer.Stop();
                            if (obd.isRunning)
                                logger.StartCollection();
                        }

                        verify++;
                    };
                obdTimeout.Start();
            }
        }

        /// <summary>
        /// Listens for an available WiFi connection and posts each log file to the gateway server.
        /// </summary>
        private void wifi_GatewayAvailable(object sender, EventArgs e)
        {
            logger.StopCollection();

            this.DumpLogs();
            //logger.reset();
        }

        #endregion

        private void DumpLogs()
        {
            //Console.Display("Checking for vehicle logs...");

            string[] logs = sdCard.GetStorageDevice().ListFiles(sdCard.GetStorageDevice().RootDirectory + Path.DirectorySeparatorChar + "Logs");
            string VIN = this.GetVIN(logs);

            string postUrl = myGatewayAddress + "id=" + VIN;

            this.PostFile(postUrl, "[");

            foreach (string log in logs)
            {
                //Console.Display("Attempting to transmit '" + log + "'...");
                this.PostFile(postUrl, log);
            }

            this.PostFile(postUrl, "]");
        }

        private int retries = 0;

        /// <summary>
        /// Creates an HTTP post request, sends the request, and prints a //Console message when the response is received.
        /// </summary>
        /// <param name="url">URL of the gateway server.</param>
        /// <param name="file">File to be uploaded.</param>
        private void PostFile(string url, string content)
        {
            try
            {
                /// Hash that is unique to this file, will be used to verify succesful transmission
                var checksum = logger.Checksum(content);

                var req = HttpHelper.CreateHttpPostRequest(url + "&checksum=" + checksum, FileToPOSTContent(content), "text");
                
                req.ResponseReceived += (sender, response) =>
                {
                    try
                    {
                        if (response.StatusCode == "BadReqeuest" && retries < 3)
                        {
                            retries++;
                            this.PostFile(url, content);
                        }

                        retries = 0;
                        //Console.Display("Server returned status: " + response.StatusCode.ToString());
                    }
                    catch (Exception ex)
                    {
                        //Console.Display("Transmission failed: " + ex.Message);
                    }
                };

                req.SendRequest();
            }
            catch (Exception e)
            {
                //Console.Display("Transmission failed: " + e.Message);
            }
        }

        /// <summary>
        /// Converts the file into a format that can be posted by the above method.
        /// </summary>
        /// <param name="content">File to be posted.</param>
        /// <returns>File in the proper POSTContent format.</returns>
        private POSTContent FileToPOSTContent(String content)
        {
            var postData = POSTContent.CreateTextBasedContent(content);

            return postData;
        }

        private string GetVIN(string[] logs)
        {
            if (obd.VIN != null)
                return obd.VIN;

            if (logs.Length == 0)
                return "VINNOTAVAILABLE";

            double samples = (logs.Length / 8);
            for (int i = 8; i > 0; i--)
            {
                double index = System.Math.Floor(i * samples);

                Snapshot[] snapshots = (Snapshot[])JsonConverter.Deserialize("[" + logger.Read(logs[(int)index]) + "]");

                foreach (Snapshot snapshot in snapshots)
                {
                    if (snapshot.VIN.Length == 17)
                        return snapshot.VIN;
                }  
            }

            return "VINNOTAVAILABLE";
        }
    }
}
